大部分复制整理于以下四篇文章,侵删致歉。

Generator函数的含义与用法
Thunk函数的含义与用法
co函数库的含义与用法
async函数的含义与用法

协程coroutine

多个线程互相协作,完成异步任务。
  1. 协程A开始执行。
  2. 协程A执行到一半,进入暂停,执行权转移到协程B。
  3. (一段时间后)协程B交还执行权。
  4. 协程A恢复执行。
1
2
3
4
5
function asnycJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}

上面代码的函数 asyncJob 是一个协程。
yield 命令表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。

协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

Generator函数

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
1
2
3
4
5
6
7
8
9
10
function* gen(x){
var y = yield x + 2;
return y;
}

-----
//执行方法
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

Generator 函数不同于普通函数,是可以暂停执行的,所以函数名之前要加星号,以示区别。

整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。

调用 Generator 函数,会返回一个内部指针(即遍历器 )g

调用指针 gnext 方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的 yield 语句,上例是执行到 x + 2 为止。

换言之,next 方法的作用是分阶段执行 Generator 函数。每次调用 next方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。

value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。

数据交换

1
2
3
4
5
6
7
8
function* gen(x){
var y = yield x + 2;
return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

第一个 next 方法的 value 属性,返回表达式 x + 2 的值(3)

第二个 next 方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果(即yield 语句后面表达式的值),被函数体内的变量 y 接收。因此,这一步的 value 属性,返回的就是2(变量 y 的值)

错误处理

捕获函数体外错误

1
2
3
4
5
6
7
8
9
10
11
12
13
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}

var g = gen(1);
g.next();
g.throw('出错了');
// 出错了

上面代码的最后一行,Generator 函数体外,使用指针对象的 throw 方法抛出的错误,可以被函数体内的 try ... catch 代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

实例

Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var fetch = require('node-fetch');

function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}

-------
//执行函数

var g = gen();
var result = g.next();

//Fetch 模块返回的是一个 Promise 对象,要用 then 方法调用下一个next 方法
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});

Thunk

Thunk 函数的含义和用法

1. 传值策略

1
2
3
4
5
6
7
8

var x = 1;

function f(m){
return m * 2;
}

f(x + 5)

“传值调用”(call by value): f(x + 5)即f(6)
“传名调用”(call by name): f(x + 5)即(x + 5) * 2

2. Thunk函数

Thunk 函数是传名调用的一种实现策略,用来替换某个表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function f(m){
return m * 2;
}

f(x + 5);

// 等同于

var thunk = function () {
return x + 5;
};

function f(thunk){
return thunk() * 2;
}

3. Thunk 函数(JavaScript)

在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数

1
2
3
4
5
6
7
8
9
10
11
12
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var readFileThunk = Thunk(fileName);
readFileThunk(callback);

var Thunk = function (fileName){
return function (callback){
return fs.readFile(fileName, callback);
};
};
Thunk 函数转换器
1
2
3
4
5
6
7
8
9
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};
1
2
3
// fs.readFile 的 Thunk 函数
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);

4. Array.prototype.slice.call(arguments)

arrayObj.slice(start, [end]):
截取数组的一部分
call([thisObj[,arg1[arg2[[argN]]]]]):
thisObj是一个对象的方法
arrg1~argN是参数

所以该方法用来把调用方法的参数截取出来

1
2
3
4
5
6
7
function test(a,b,c,d)
{
var arg = Array.prototype.slice.call(arguments,1);
//约等于 arguments.slice(1)
alert(arg);
}
test("a","b","c","d"); //b,c,d

因为arguments并不是真正的数组对象而是Object,只是与数组类似而已,所以它并没有slice这个方法,而Array.prototype.slice.call(arguments, 1)可以理解成是让arguments转换成一个数组对象,让arguments具有slice()方法。要是直接写arguments.slice(1)会报错。

原理

`Array.prototype.slice.call(arguments)能将具有length属性的对象转成数组,除了IE下的节点集合(因为ie下的dom对象是以com对象的形式实现的,js对象与com对象不能进行转换)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var a={length:2,0:'first',1:'second'};
//类数组,有length属性,长度为2,第0个是first,第1个是second
console.log(Array.prototype.slice.call(a,0));
// ["first", "second"],调用数组的slice(0);

var a={length:2,0:'first',1:'second'};
console.log(Array.prototype.slice.call(a,1));
//["second"],调用数组的slice(1);

var a={0:'first',1:'second'};
//去掉length属性,返回一个空数组
console.log(Array.prototype.slice.call(a,0));
//[]

function test(){
console.log(Array.prototype.slice.call(arguments,0));
//["a", "b", "c"],slice(0)
console.log(Array.prototype.slice.call(arguments,1));
//["b", "c"],slice(1)
}
test("a","b","c");
将函数的实际参数转换成数组的方法
  1. var args = Array.prototype.slice.call(arguments);
  2. var args = [].slice.call(arguments, 0);
  3. 1
    2
    3
    4
    var args = [];
    for (var i = 1; i < arguments.length; i++) {
    args.push(arguments[i]);
    }

5. Thunkify

https://github.com/tj/node-thunkify

Turn a regular node function into one which returns a thunk, useful for generator-based flow control such as co.
1
2
3
4
5
6
7
8
var thunkify = require('thunkify');
var fs = require('fs');

var read = thunkify(fs.readFile);

read('package.json', 'utf8')(function(err, str){

});

6. 基于 Thunk 函数的 Generator 执行器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function run(fn) {
var gen = fn();

function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}

next();
}

var gen = function* (){
var f1 = yield readFile('fileA');
var f2 = yield readFile('fileB');
// ...
var fn = yield readFile('fileN');
};

run(gen);

上面代码的 run 函数,就是一个 Generator 函数的自动执行器。
内部的 next 函数就是 Thunk 的回调函数。
next 函数先将指针移到 Generator 函数的下一步(gen.next 方法),然后判断 Generator 函数是否结束(result.done 属性),如果没结束,就将 next 函数再传入 Thunk 函数(result.value 属性),否则就直接退出。

函数gen 封装了 n 个异步的读取文件操作,只要执行 run 函数,这些操作就会自动完成。

CO

co

The ultimate generator based flow-control goodness for nodejs (supports thunks, promises, etc)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Generator 函数
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

//Generator 函数只要传入 co 函数,就会自动执行
var co = require('co');
co(gen);

//co 函数返回一个 Promise 对象
co(gen).then(function (){
console.log('Generator 函数执行完成');
})

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//只使用回调函数
var fs = require('fs');
function readFile(path, cb) {
fs.readFile(path, {encoding: 'utf8'}, cb);
}

readFile('a.js', function (err, dataA) {
console.log(dataA);
readFile('b.js', function (err, dataB) {
console.log(dataB);
readFile('c.js', function (err, dataC) {
console.log(dataC);
...
});
});
});

----------
//使用CO
var fs = require('fs');
var co = require('co');

function readFile(path) {
return function (cb) {
fs.readFile(path, {encoding: 'utf8'}, cb);
};
}

co(function* () {
var dataA = yield readFile('a.js');
console.log(dataA);
var dataB = yield readFile('b.js');
console.log(dataB);
var dataC = yield readFile('c.js');
console.log(dataC);
}).catch(function (err) {
console.log(err);
});

co 将所有 yield 后面的表达式都封装成了 Promise 对象(本身也返回一个Promise 对象),只有当前表达式执行结束后(即调用 .then),然后会在 onFulfilled 函数内执行 gen.next(res) 将 res 赋值给 yield 左侧的变量并执行到下一个 yield,下一个表达式执行结束后又调用 gen.next(),如此循环,直至 done 变为 true。

ES6 中的 yield 后面可以跟任意类型的值,但 co 对此做了限制,只允许 yield 后跟 thunk, promise, generator, generatorFunction,array 或者 object

原理

co 函数库其实就是将两种自动执行器(Thunk 函数Promise 对象),包装成一个库。

  1. 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
  2. Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。

使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//co 函数接受 Generator 函数作为参数,返回一个 Promise 对象
function co(gen) {
var ctx = this;

return new Promise(function(resolve, reject) {

//co 先检查参数 gen 是否为 Generator 函数。如果是,就执行该函数,得到一个内部指针对象
if (typeof gen === 'function') gen = gen.call(ctx);
//如果不是就返回,并将 Promise 对象的状态改为 resolved
if (!gen || typeof gen.next !== 'function') return resolve(gen);

//co 将 Generator 函数的内部指针对象的 next 方法,包装成 onFulefilled 函数。这主要是为了能够捕捉抛出的错误。
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}

function next(ret) {

//检查当前是否为 Generator 函数的最后一步,如果是就返回
if (ret.done) return resolve(ret.value);
//确保每一步的返回值,是 Promise 对象
var value = toPromise.call(ctx, ret.value);
//使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
//在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});

并发

把并发的操作都放在数组或对象里面, co允许某些操作同时进行,等到它们全部完成,才进行下一步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 数组的写法
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);

// 对象的写法
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
};
console.log(res);
}).catch(onerror);

async

async 函数就是 Generator 函数的语法糖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var fs = require('fs');

var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error);
resolve(data);
});
});
};

var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

-----------
//async
var asyncReadFile = async function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await

优点

(1)内置执行器。
Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。

var result = asyncReadFile();

(2)更好的语义。
async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。

(3)更广的适用性。
co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

实现

async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
async function fn(args){
// ...
}

----------
// 等同于
function fn(args){
return spawn(function*() {
// ...
});
}

//spawn 函数就是自动执行器
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}

使用

同 Generator 函数一样,async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。
当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value)
}

asyncPrint('hello world', 50);

-------------
//50毫秒以后,输出"hello world"

await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}

// 另一种写法

async function myFunction() {
await somethingThatReturnsAPromise().catch(function (err){
console.log(err);
});
}

await 命令只能用在 async 函数之中。

可以使用 Promise.all 方法使多个请求并发执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));

let results = await Promise.all(promises);
console.log(results);
}

// 或者使用下面的写法

async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));

let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}